﻿using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SilverTile;
using System.Windows.Media.Imaging;
using System.Windows.Media;
using Rhino.Mocks;
using SilverTile.ExtensionMethods;
using SilverTile.Sprites;
using SilverTile.Viewports;
using SilverTile.Xaml;
using System.Windows;
using System.Windows.Markup;

namespace SilverTileTests
{
    [TestClass]
    public class ViewportTests
    {


        string xamlWithTransitions = @"
                    <ResourceDictionary
                        xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"" 
                        xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
                        xmlns:st=""clr-namespace:SilverTile.Xaml;assembly=SilverTile"">
                        <st:SpriteFrame x:Key=""Sand"" Source=""/SilverTileTests;component/Tiles/0002.png"" />    

                        <st:GameAreaGrid x:Key=""Area1"" AreaName=""Area1"" Columns=""2"" Rows=""2"" TileWidth=""32"" TileHeight=""32"">
                            <st:SpriteTile X=""0"" Y=""0"" Blocking=""False"" Frame=""{StaticResource Sand}"" >
                                <st:SpriteTile.Transition>
                                    <st:AreaTransition GameAreaName=""Area2"" TargetRow=""0"" TargetColumn=""0"" />
                                </st:SpriteTile.Transition>
                            </st:SpriteTile>
                            <st:SpriteTile X=""1"" Y=""0"" Blocking=""False"" Frame=""{StaticResource Sand}"" />
                            <st:SpriteTile X=""0"" Y=""1"" Blocking=""False"" Frame=""{StaticResource Sand}"" />
                            <st:SpriteTile X=""1"" Y=""1"" Blocking=""False"" Frame=""{StaticResource Sand}"" />
                        </st:GameAreaGrid>
                        <st:GameAreaGrid x:Key=""Area2"" AreaName=""Area2"" Columns=""2"" Rows=""2"" TileWidth=""32"" TileHeight=""32"">
                            <st:SpriteTile X=""0"" Y=""0"" Blocking=""False"" Frame=""{StaticResource Sand}"" />
                            <st:SpriteTile X=""1"" Y=""0"" Blocking=""False"" Frame=""{StaticResource Sand}"" />
                            <st:SpriteTile X=""0"" Y=""1"" Blocking=""False"" Frame=""{StaticResource Sand}"" />
                            <st:SpriteTile X=""1"" Y=""1"" Blocking=""False"" Frame=""{StaticResource Sand}"" >
                                <st:SpriteTile.Transition>
                                    <st:AreaTransition GameAreaName=""Area1"" TargetRow=""1"" TargetColumn=""1"" />
                                </st:SpriteTile.Transition>
                            </st:SpriteTile>
                        </st:GameAreaGrid>
                    </ResourceDictionary>
                    ";

        private MockRepository _mockRepository;

        [TestInitialize]
        public void Initialize()
        {
            _mockRepository = new MockRepository();
        }

        [TestMethod]
        public void Viewport_Should_Have_A_Size()
        {
            int expectedWidth = 100;
            int expectedHeight = 200;
            IViewport viewport = new Viewport(expectedWidth, expectedHeight);

            Assert.AreEqual(expectedWidth, viewport.Width, "Width should be " + expectedWidth);
            Assert.AreEqual(expectedHeight, viewport.Height, "Height should be " + expectedHeight);

        }

        [TestMethod]
        public void Viewport_Should_Implement_IViewport_And_IUpdateable()
        {
            var viewport = new Viewport(1, 1);
            Assert.IsInstanceOfType(viewport, typeof(IViewport), "Viewport is not IViewport");
            Assert.IsInstanceOfType(viewport, typeof(IUpdateable), "Viewport is not IUpdateable");
        }

        [TestMethod]
        public void Viewport_Should_Expose_A_BitmapSource_For_The_Output()
        {
            IViewport viewport = new Viewport(10, 10);
            Assert.IsInstanceOfType(viewport.Output, typeof(BitmapSource), "Output is not BitmapSource");
        }

        [TestMethod]
        public void Viewport_Output_Should_Have_Correct_Size()
        {
            int expectedWidth = 100;
            int expectedHeight = 200;
            IViewport viewport = new Viewport(expectedWidth, expectedHeight);

            Assert.AreEqual(expectedWidth, viewport.Output.PixelWidth, "Output width should be " + expectedWidth);
            Assert.AreEqual(expectedHeight, viewport.Output.PixelHeight, "Output height should be " + expectedHeight);
        }

        [TestMethod]
        public void Background_Color_Should_Default_To_Black()
        {

            IViewport viewport = new Viewport(10, 10);
            Assert.AreEqual(Colors.Black, viewport.BackgroundColor, "Default background color is incorrect");

        }

        [TestMethod]
        public void Background_Color_Should_Be_Changeable()
        {

            IViewport viewport = new Viewport(10, 10)
            {
                BackgroundColor = Colors.Purple
            };
            Assert.AreEqual(Colors.Purple, viewport.BackgroundColor, "Background color was not set correctly");

        }

        [TestMethod]
        public void Viewport_Should_Have_Activce_Sprites_Dictionary()
        {

            IViewport viewport = new Viewport(10, 10);
            Assert.IsNotNull(viewport.ActiveSprites, "ActiveSprites dictionary is null");
            Assert.IsInstanceOfType(viewport.ActiveSprites, typeof(IDictionary<string, ISprite>), "ActiveSprites is not IDictionary<string, ISprite>");

        }

        [TestMethod]
        public void Viewport_Should_Have_Background_Sprites_Collection()
        {

            IViewport viewport = new Viewport(10, 10);
            Assert.IsNotNull(viewport.BackgroundSprites, "BackgroundSprites collection is null");
            Assert.IsInstanceOfType(viewport.BackgroundSprites, typeof(IEnumerable<ISprite>), "Background sprites collection is not IEnumerable<ISprite>");

        }

        [TestMethod]
        public void Add_Active_Sprite_Should_Update_Active_Sprites_Dictionary()
        {

            ISprite sprite = _mockRepository.StrictMock<ISprite>();
            Expect.Call(sprite.Viewport).SetPropertyAndIgnoreArgument();
            Expect.Call(sprite.Z).PropertyBehavior();
            _mockRepository.ReplayAll();

            IViewport viewport = new Viewport(10, 10);
            viewport.AddSprite("sprite", sprite);

            Assert.AreSame(sprite, viewport.ActiveSprites["sprite"], "sprite variable and sprite in dictionary are not the same");
            Assert.AreEqual(1, viewport.ActiveSprites.Count, "Number of active sprites is not 1");

        }

        [TestMethod]
        public void Add_Active_Sprite_Should_Allow_ZOrder_To_Be_Specified()
        {

            ISprite sprite = _mockRepository.StrictMock<ISprite>();
            Expect.Call(sprite.Viewport).SetPropertyAndIgnoreArgument();
            Expect.Call(sprite.Z).PropertyBehavior();

            _mockRepository.ReplayAll();

            IViewport viewport = new Viewport(10, 10);
            viewport.AddSprite("sprite", sprite, -1);

            Assert.AreEqual(-1, sprite.Z, "Sprite.Z was not set by AddSprite");

        }


        [TestMethod]
        public void Remove_Sprite_By_Key_Should_Update_Active_Sprites_Dictionary_And_Return_The_Removed_Sprite()
        {

            ISprite sprite = _mockRepository.StrictMock<ISprite>();
            Expect.Call(sprite.Viewport).SetPropertyAndIgnoreArgument();
            Expect.Call(sprite.Z).PropertyBehavior();
            _mockRepository.ReplayAll();

            IViewport viewport = new Viewport(10, 10);
            viewport.AddSprite("sprite", sprite);
            ISprite removed = viewport.RemoveSprite("sprite");

            Assert.AreSame(sprite, removed, "Removed sprite is not the same as the added sprite");
            Assert.AreEqual(0, viewport.ActiveSprites.Count, "Number of active sprites is not 0");

        }

        [TestMethod]
        public void Remove_Sprite_By_Ref_Should_Update_Active_Sprites_Dictionary_And_Return_The_Removed_Sprite()
        {

            ISprite sprite = _mockRepository.StrictMock<ISprite>();
            Expect.Call(sprite.Viewport).SetPropertyAndIgnoreArgument();
            Expect.Call(sprite.Z).PropertyBehavior();
            _mockRepository.ReplayAll();

            IViewport viewport = new Viewport(10, 10);
            viewport.AddSprite("sprite", sprite);
            ISprite removed = viewport.RemoveSprite(sprite);

            Assert.AreSame(sprite, removed, "Removed sprite is not the same as the added sprite");
            Assert.AreEqual(0, viewport.ActiveSprites.Count, "Number of active sprites is not 0");

        }

        [TestMethod]
        [ExpectedException(typeof(ArgumentOutOfRangeException))]
        public void Remove_Sprite_With_Wrong_Key_Should_Throw_Exception()
        {

            IViewport viewport = new Viewport(10, 10);
            viewport.RemoveSprite("does not exist");

        }

        [TestMethod]
        [ExpectedException(typeof(ArgumentOutOfRangeException))]
        public void Remove_Sprite_With_Wrong_Reference_Should_Throw_Exception()
        {

            IViewport viewport = new Viewport(10, 10);
            ISprite sprite = _mockRepository.StrictMock<ISprite>();
            viewport.RemoveSprite(sprite);

        }

        [TestMethod]
        public void Add_Background_Sprite_Should_Update_Background_Sprites_Dictionary()
        {

            ISprite sprite = _mockRepository.StrictMock<ISprite>();
            Expect.Call(sprite.X).SetPropertyAndIgnoreArgument();
            Expect.Call(sprite.Y).SetPropertyAndIgnoreArgument();
            Expect.Call(sprite.Viewport).SetPropertyAndIgnoreArgument();
            _mockRepository.ReplayAll();

            IViewport viewport = new Viewport(10, 10);
            viewport.AddBackgroundSprite(sprite, 0, 0);

            Assert.AreSame(sprite, viewport.BackgroundSprites.First(), "sprite variable and sprite in background sprites are not the same");
            Assert.AreEqual(1, viewport.BackgroundSprites.Count(), "Number of background sprites is not 1");

        }

        [TestMethod]
        public void Add_Background_Sprite_Should_Set_X_And_Y_Viewport()
        {

            IViewport viewport = new Viewport(10, 10);
            ISprite sprite = _mockRepository.DynamicMock<ISprite>();
            Expect.Call(sprite.X).PropertyBehavior();
            Expect.Call(sprite.Y).PropertyBehavior();
            Expect.Call(sprite.Viewport).PropertyBehavior();

            int expectedX = 10;
            int expectedY = 12;

            viewport.AddBackgroundSprite(sprite, expectedX, expectedY);

            Assert.AreEqual(expectedX, sprite.X, "sprite.X was not set correctly");
            Assert.AreEqual(expectedY, sprite.Y, "sprite.Y was not set correctly");
            Assert.AreSame(viewport, sprite.Viewport, "sprite.Viewport was not set correctly");

        }

        [TestMethod]
        public void Viewport_Render_Should_Not_Call_Sprite_Render_For_Background_Sprites()
        {


            ISprite bgSprite = _mockRepository.StrictMock<Sprite>("/SilverTileTests;component/Tiles/0002.png");
            Expect.Call(bgSprite.X).PropertyBehavior();
            Expect.Call(bgSprite.Y).PropertyBehavior();
            Expect.Call(bgSprite.Viewport).PropertyBehavior();
            Expect.Call(bgSprite.OriginX).Return(0);
            Expect.Call(bgSprite.OriginY).Return(0);

            IViewport viewport = new Viewport(10, 10);
            viewport.AddBackgroundSprite(bgSprite, 0, 0);

            _mockRepository.ReplayAll();
            viewport.Render();
            try
            {
                _mockRepository.VerifyAll();
            }
            catch (Rhino.Mocks.Exceptions.ExpectationViolationException ex)
            {
                Assert.Fail("Render method was called for background sprite." + Environment.NewLine + ex.Message);
            }

        }

        [TestMethod]
        public void Viewport_Render_Should_Render_Sprites_According_To_Z_Ascending()
        {


            // Create viewport and sprite
            IViewport viewport = new Viewport(5, 5);
            ISprite sprite1 = new Sprite(5, 5);
            ISprite sprite2 = new Sprite(5, 5);
            
            // Set sprite1's pixels
            int[] pixels1 = new int[25];
            for (int p = 0; p < 25; p++)
            {
                pixels1[p] = Colors.Red.ToInt32();
            }
            Array.Copy(pixels1, sprite1.Pixels, 25);

            // Set sprite2's pixels
            int[] pixels2 = new int[25];
            for (int p = 0; p < 25; p++)
            {
                pixels2[p] = Colors.Blue.ToInt32();
            }
            Array.Copy(pixels2, sprite2.Pixels, 25);

            viewport.AddSprite("1", sprite1); // Z = 0
            viewport.AddSprite("2", sprite2, -1); // Z=  -1, behind sprite1


            // Render 
            viewport.Render();
            
            // Ensure that the output is red, not blue
            int[] outputBuffer = ((WriteableBitmap)viewport.Output).Pixels;
            CollectionAssert.AreEqual(pixels1, outputBuffer, "Sprite1 was not rendered about sprite2");

        }

        [TestMethod]
        public void Viewport_Update_Should_Call_Update_For_All_Sprites()
        {

            IViewport viewport = new Viewport(10, 10);
            ISprite fgSprite = _mockRepository.PartialMock<Sprite>(10, 10);
            ISprite bgSprite = _mockRepository.PartialMock<Sprite>(10, 10);

            Expect.Call(delegate { fgSprite.Update(TimeSpan.FromMilliseconds(10)); }).IgnoreArguments().TentativeReturn();
            Expect.Call(delegate { bgSprite.Update(TimeSpan.FromMilliseconds(10)); }).IgnoreArguments().TentativeReturn();
            Expect.Call(bgSprite.X).PropertyBehavior();
            Expect.Call(bgSprite.Y).PropertyBehavior();

            _mockRepository.ReplayAll();

            viewport.AddSprite("sprite", fgSprite);
            viewport.AddBackgroundSprite(bgSprite, 0, 0);

            viewport.Update(TimeSpan.FromMilliseconds(20));
            try
            {
                _mockRepository.VerifyAll();
            }
            catch (Rhino.Mocks.Exceptions.ExpectationViolationException ex)
            {
                Assert.Fail("Update methods were not called." + Environment.NewLine + ex.Message);
            }

        }

        [TestMethod]
        public void Add_Sprite_Should_Set_The_Sprites_Viewport()
        {

            IViewport viewport = new Viewport(5, 5);
            ISprite sprite = new Sprite(5, 5);

            viewport.AddSprite("", sprite);

            Assert.AreSame(viewport, sprite.Viewport, "Sprite.Viewport was not set correctly");

        }

        [TestMethod]
        public void Viewport_Render_Should_Draw_To_WriteableBitmap()
        {

            // Create viewport and sprite
            IViewport viewport = new Viewport(5, 5);
            ISprite sprite = new Sprite(5, 5) { X = 0, Y = 0 };
            viewport.AddSprite("", sprite);

            // Set the sprites pixels
            int[] pixels = new int[25];
            for (int p = 0; p < 25; p++)
            {
                pixels[p] = Color.FromArgb(255, (byte)p, 0, 0).ToInt32();
            }
            Array.Copy(pixels, sprite.Pixels, 25);

            // Render 
            viewport.Render();

            // Verify that the output pixels are the same as the sprites pixels
            int[] ouputPixels = viewport.Pixels; // ((WriteableBitmap)viewport.Output).Pixels;
            CollectionAssert.AreEqual(pixels, ouputPixels, "Pixels in output are not the same as for the sprite");

        }


        [TestMethod]
        public void Viewport_Render_With_Sprite_Outside_Area_Should_Render_Nothing()
        {

            // Create viewport and sprite
            IViewport viewport = new Viewport(5, 5);
            ISprite sprite = new Sprite(5, 5) { X = -10, Y = -10 };
            viewport.AddSprite("", sprite);

            // Set the sprites pixels
            int[] pixels = new int[25];
            for (int p = 0; p < 25; p++)
            {
                pixels[p] = Color.FromArgb(255, (byte)p, 0, 0).ToInt32();
            }
            Array.Copy(pixels, sprite.Pixels, 25);

            // Render 
            viewport.Render();

            // Verify that the output pixels are the same as the sprites pixels
            int[] ouputPixels = ((WriteableBitmap)viewport.Output).Pixels;

            int[] emptyBuffer = new int[25];
            for (int p = 0; p < 25; p++)
            {
                emptyBuffer[p] = Colors.Black.ToInt32();
            }

            CollectionAssert.AreEqual(emptyBuffer, ouputPixels, "Pixels in output are not the same as for the empty buffer");

        }

        [TestMethod]
        public void Viewport_Render_With_Partial_Visibility_Should_Render_Some_Pixels()
        {

            // Create viewport and sprite
            IViewport viewport = new Viewport(5, 5);
            ISprite sprite = new Sprite(5, 5) { X = 3, Y = 3 }; // Sprite is partially visible
            viewport.AddSprite("", sprite);

            // Set the sprites pixels
            int[] pixels = new int[25];
            for (int p = 0; p < 25; p++)
            {
                pixels[p] = Color.FromArgb(255, (byte)p, 0, 0).ToInt32();
            }
            Array.Copy(pixels, sprite.Pixels, 25);

            // Render 
            viewport.Render();

            // Verify that the output pixels are the same as the sprites pixels
            int[] ouputPixels = ((WriteableBitmap)viewport.Output).Pixels;

            for (int x = 0; x < 5; x++)
            {
                for (int y = 0; y < 5; y++)
                {

                    int expectedPixel = viewport.BackgroundColor.ToInt32();
                    if (x >= sprite.X && y >= sprite.Y)
                    {
                        int expPixOffset = (x - sprite.X) + (y - sprite.Y) * sprite.Width;
                        expectedPixel = sprite.Pixels[expPixOffset];
                    }

                    int realPixOffset = x + y * viewport.Width;
                    int renderedPixel = ouputPixels[realPixOffset];

                    Assert.AreEqual(expectedPixel, renderedPixel, "Pixel at (" + x + "," + y + ") do not match the expected value");

                }
            }

        }

        [TestMethod]
        public void Large_Sprite_Should_Fill_Output()
        {

            // Create viewport and sprite
            IViewport viewport = new Viewport(5, 5);
            ISprite sprite = new Sprite(10, 10) { X = -2, Y = -2 };
            viewport.AddSprite("", sprite);

            // Set the sprites pixels
            int[] pixels = new int[100];
            for (int p = 0; p < 100; p++)
            {
                pixels[p] = Color.FromArgb(255, (byte)p, 0, 0).ToInt32();
            }
            Array.Copy(pixels, sprite.Pixels, 100);

            // Render 
            viewport.Render();

            // Verify that the output pixels are the same as the sprites pixels
            int[] ouputPixels = ((WriteableBitmap)viewport.Output).Pixels;

            for (int x = 0; x < 5; x++)
            {
                for (int y = 0; y < 5; y++)
                {

                    int expPixOffset = (x - sprite.X) + (y - sprite.Y) * sprite.Width;
                    int expectedPixel = sprite.Pixels[expPixOffset];

                    int realPixOffset = x + y * viewport.Width;
                    int renderedPixel = ouputPixels[realPixOffset];

                    Assert.AreEqual(expectedPixel, renderedPixel, "Pixel at (" + x + "," + y + ") do not match the expected value");

                }
            }

        }

        [TestMethod]
        public void Hidden_Sprite_Should_Not_Be_Rendered()
        {
            // Create viewport and sprite
            IViewport viewport = new Viewport(5, 5);
            ISprite sprite = new Sprite(5, 5) { X = 0, Y = 0, IsVisible = false };
            viewport.AddSprite("", sprite);

            // Set the sprites pixels
            int[] pixels = new int[25];
            for (int p = 0; p < 25; p++)
            {
                pixels[p] = Color.FromArgb(255, (byte)p, 0, 0).ToInt32();
            }
            Array.Copy(pixels, sprite.Pixels, 25);

            // Render 
            viewport.Render();

            // Verify that the output pixels are the same as the sprites pixels
            int[] ouputPixels = ((WriteableBitmap)viewport.Output).Pixels;

            int[] emptyBuffer = new int[25];
            for (int p = 0; p < 25; p++)
            {
                emptyBuffer[p] = Colors.Black.ToInt32();
            }

            CollectionAssert.AreEqual(emptyBuffer, ouputPixels, "Pixels in output are not the same as for the empty buffer");

        }

        [TestMethod]
        public void Viewport_Render_Should_Respect_Origin_Values()
        {

            // Set the frames pixels
            IFrame frame = new Frame(5, 5) { OriginX = 2, OriginY = 2 };
            int[] pixels = new int[25];
            for (int p = 0; p < 25; p++)
            {
                pixels[p] = Color.FromArgb(255, (byte)p, 0, 0).ToInt32();
            }
            Array.Copy(pixels, frame.Pixels, 25);

            // Create viewport and sprite
            IViewport viewport = new Viewport(5, 5);
            ISprite sprite = new Sprite(frame);
            viewport.AddSprite("", sprite);
            viewport.BackgroundColor = Colors.Black;

            // Render 
            viewport.Render();

            // Verify that the output pixels are the same as the sprites pixels
            int[] ouputPixels = ((WriteableBitmap)viewport.Output).Pixels;

            int[] expectedPixels = new int[25];

            // Perform rendering manually
            for (int x = 0; x < 5; x++)
            {
                for (int y = 0; y < 5; y++)
                {

                    // Get sprite pixel at this position
                    int spriteX = x + sprite.OriginX;
                    int spriteY = y + sprite.OriginY;

                    int pixel = Colors.Black.ToInt32();
                    if (spriteX < sprite.Width && spriteY < sprite.Height)
                    {
                        pixel = sprite.Pixels[spriteX + spriteY * sprite.Width];
                    }

                    expectedPixels[x + y * viewport.Width] = pixel;
                }
            }

            CollectionAssert.AreEqual(expectedPixels, ouputPixels, "Rendered output does not match expected output");

        }

        [TestMethod]
        [ExpectedException(typeof(ArgumentException))]
        public void Invalid_GameArea_Should_Not_Be_Assignable_To_Viewport()
        {

            GameAreaGrid gameArea = new GameAreaGrid
            {
                Columns = 2,
                Rows = 2,
                TileHeight = 1,
                TileWidth = 1
            };

            SpriteFrame frame = new SpriteFrame { Source = "/SilverTileTests;component/Tiles/0002.png" };

            gameArea.BackgroundSprites.Add(new SpriteTile { X = 0, Y = 0, Frame = frame });
            gameArea.BackgroundSprites.Add(new SpriteTile { X = 1, Y = 0, Frame = frame });
            gameArea.BackgroundSprites.Add(new SpriteTile { X = 0, Y = 1, Frame = frame });
            gameArea.BackgroundSprites.Add(new SpriteTile { X = 1, Y = 1, Frame = frame });
            gameArea.BackgroundSprites.Add(new SpriteTile { X = 1, Y = 0, Frame = frame });

            IViewport viewport = new Viewport(100, 100);
            viewport.SetGameArea(gameArea);

        }

        [TestMethod]
        public void GameArea_Xaml_With_Transitions_Should_Be_Parsed_Correctly()
        {

            try
            {
                ResourceDictionary dictionary = (ResourceDictionary)XamlReader.Load(xamlWithTransitions);

                Assert.IsNotNull(dictionary, "Parsed ResourceDictionary is null");
                Assert.IsInstanceOfType(dictionary["Area1"], typeof(GameAreaGrid), "Area1 was not deserialized into a GameAreaGrid");
                Assert.IsInstanceOfType(dictionary["Area2"], typeof(GameAreaGrid), "Area2 was not deserialized into a GameAreaGrid");
            }
            catch (Exception ex)
            {
                Assert.Fail(ex.ToString());
            }
        }

    }
}
